Transmission¶
Model energy or material transport between locations with losses.
This notebook covers:
- Transmission component: Connecting sites with pipelines, cables, or conveyors
- Transmission losses: Relative losses (proportional) and absolute losses (fixed)
- Bidirectional flow: Two-way transmission with flow direction constraints
- Capacity optimization: Sizing transmission infrastructure
Setup¶
In [1]:
Copied!
import numpy as np
import pandas as pd
import plotly.express as px
import xarray as xr
import flixopt as fx
fx.CONFIG.notebook()
import numpy as np import pandas as pd import plotly.express as px import xarray as xr import flixopt as fx fx.CONFIG.notebook()
Out[1]:
flixopt.config.CONFIG
The Problem: Connecting Two Sites¶
Consider a district heating network with two sites:
- Site A: Has a large gas boiler (cheap production)
- Site B: Has a smaller electric boiler (expensive, but flexible)
A district heating pipe connects both sites. The question: How should heat flow between sites to minimize total costs?
Transmission Characteristics¶
| Parameter | Value | Description |
|---|---|---|
| Relative losses | 5% | Heat loss proportional to flow (pipe heat loss) |
| Capacity | 200 kW | Maximum transmission rate |
| Bidirectional | Yes | Heat can flow A→B or B→A |
Define Time Series Data¶
In [2]:
Copied!
# One week simulation
timesteps = pd.date_range('2024-01-22', periods=168, freq='h')
hours = np.arange(168)
hour_of_day = hours % 24
# Site A: Industrial facility with steady demand
demand_a_base = 150
demand_a_variation = 30 * np.sin(hour_of_day * np.pi / 12) # Day/night cycle
demand_a = demand_a_base + demand_a_variation
# Site B: Office building with peak during work hours
demand_b = np.where(
(hour_of_day >= 8) & (hour_of_day <= 18),
180, # Daytime: 180 kW
80, # Nighttime: 80 kW
)
# Add weekly pattern (lower on weekends)
day_of_week = (hours // 24) % 7
demand_b = np.where(day_of_week >= 5, demand_b * 0.6, demand_b) # Weekend reduction
# One week simulation timesteps = pd.date_range('2024-01-22', periods=168, freq='h') hours = np.arange(168) hour_of_day = hours % 24 # Site A: Industrial facility with steady demand demand_a_base = 150 demand_a_variation = 30 * np.sin(hour_of_day * np.pi / 12) # Day/night cycle demand_a = demand_a_base + demand_a_variation # Site B: Office building with peak during work hours demand_b = np.where( (hour_of_day >= 8) & (hour_of_day <= 18), 180, # Daytime: 180 kW 80, # Nighttime: 80 kW ) # Add weekly pattern (lower on weekends) day_of_week = (hours // 24) % 7 demand_b = np.where(day_of_week >= 5, demand_b * 0.6, demand_b) # Weekend reduction
In [3]:
Copied!
# Visualize demand profiles
fig = px.line(
x=timesteps.tolist() * 2,
y=np.concatenate([demand_a, demand_b]),
color=['Site A (Industrial)'] * 168 + ['Site B (Office)'] * 168,
title='Heat Demand at Both Sites',
labels={'x': 'Time', 'y': 'Heat Demand [kW]', 'color': 'Site'},
)
fig
# Visualize demand profiles fig = px.line( x=timesteps.tolist() * 2, y=np.concatenate([demand_a, demand_b]), color=['Site A (Industrial)'] * 168 + ['Site B (Office)'] * 168, title='Heat Demand at Both Sites', labels={'x': 'Time', 'y': 'Heat Demand [kW]', 'color': 'Site'}, ) fig
Example 1: Unidirectional Transmission¶
Start with a simple case: heat flows only from Site A to Site B.
In [4]:
Copied!
fs_unidirectional = fx.FlowSystem(timesteps)
fs_unidirectional.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
fs_unidirectional.add_elements(
# === Buses (one per site) ===
fx.Bus('Heat_A', carrier='heat'), # Site A heat network
fx.Bus('Heat_B', carrier='heat'), # Site B heat network
fx.Bus('Gas', carrier='gas'), # Gas supply network
fx.Bus('Electricity', carrier='electricity'), # Electricity grid
# === Effect ===
fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True),
# === External supplies ===
fx.Source('GasSupply', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour=0.06)]),
fx.Source('ElecGrid', outputs=[fx.Flow('Elec', bus='Electricity', size=500, effects_per_flow_hour=0.25)]),
# === Site A: Large gas boiler (cheap) ===
fx.LinearConverter(
'GasBoiler_A',
inputs=[fx.Flow('Gas', bus='Gas', size=500)],
outputs=[fx.Flow('Heat', bus='Heat_A', size=400)],
conversion_factors=[{'Gas': 1, 'Heat': 0.92}], # 92% efficiency
),
# === Site B: Small electric boiler (expensive but flexible) ===
fx.LinearConverter(
'ElecBoiler_B',
inputs=[fx.Flow('Elec', bus='Electricity', size=250)],
outputs=[fx.Flow('Heat', bus='Heat_B', size=250)],
conversion_factors=[{'Elec': 1, 'Heat': 0.99}], # 99% efficiency
),
# === Transmission: A → B (unidirectional) ===
fx.Transmission(
'Pipe_A_to_B',
in1=fx.Flow('from_A', bus='Heat_A', size=200), # Input from Site A
out1=fx.Flow('to_B', bus='Heat_B', size=200), # Output to Site B
relative_losses=0.05, # 5% heat loss in pipe
),
# === Demands ===
fx.Sink('Demand_A', inputs=[fx.Flow('Heat', bus='Heat_A', size=1, fixed_relative_profile=demand_a)]),
fx.Sink('Demand_B', inputs=[fx.Flow('Heat', bus='Heat_B', size=1, fixed_relative_profile=demand_b)]),
)
fs_unidirectional.optimize(fx.solvers.HighsSolver())
fs_unidirectional = fx.FlowSystem(timesteps) fs_unidirectional.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) fs_unidirectional.add_elements( # === Buses (one per site) === fx.Bus('Heat_A', carrier='heat'), # Site A heat network fx.Bus('Heat_B', carrier='heat'), # Site B heat network fx.Bus('Gas', carrier='gas'), # Gas supply network fx.Bus('Electricity', carrier='electricity'), # Electricity grid # === Effect === fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True), # === External supplies === fx.Source('GasSupply', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour=0.06)]), fx.Source('ElecGrid', outputs=[fx.Flow('Elec', bus='Electricity', size=500, effects_per_flow_hour=0.25)]), # === Site A: Large gas boiler (cheap) === fx.LinearConverter( 'GasBoiler_A', inputs=[fx.Flow('Gas', bus='Gas', size=500)], outputs=[fx.Flow('Heat', bus='Heat_A', size=400)], conversion_factors=[{'Gas': 1, 'Heat': 0.92}], # 92% efficiency ), # === Site B: Small electric boiler (expensive but flexible) === fx.LinearConverter( 'ElecBoiler_B', inputs=[fx.Flow('Elec', bus='Electricity', size=250)], outputs=[fx.Flow('Heat', bus='Heat_B', size=250)], conversion_factors=[{'Elec': 1, 'Heat': 0.99}], # 99% efficiency ), # === Transmission: A → B (unidirectional) === fx.Transmission( 'Pipe_A_to_B', in1=fx.Flow('from_A', bus='Heat_A', size=200), # Input from Site A out1=fx.Flow('to_B', bus='Heat_B', size=200), # Output to Site B relative_losses=0.05, # 5% heat loss in pipe ), # === Demands === fx.Sink('Demand_A', inputs=[fx.Flow('Heat', bus='Heat_A', size=1, fixed_relative_profile=demand_a)]), fx.Sink('Demand_B', inputs=[fx.Flow('Heat', bus='Heat_B', size=1, fixed_relative_profile=demand_b)]), ) fs_unidirectional.optimize(fx.solvers.HighsSolver())
Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms LP linopy-problem-vm7u9y1a has 1864 rows; 2368 cols; 6068 nonzeros Coefficient ranges: Matrix [6e-02, 1e+00] Cost [1e+00, 1e+00] Bound [5e+01, 1e+03] RHS [0e+00, 0e+00] Presolving model 168 rows, 336 cols, 336 nonzeros 0s 0 rows, 0 cols, 0 nonzeros 0s Presolve reductions: rows 0(-1864); columns 0(-2368); nonzeros 0(-6068) - Reduced to empty Performed postsolve Solving the original LP from the solution after postsolve Model name : linopy-problem-vm7u9y1a Model status : Optimal Objective value : 2.4790029474e+03 P-D objective error : 4.5850656416e-16 HiGHS run time : 0.01
Out[4]:
FlowSystem ========== Timesteps: 168 (Hour) [2024-01-22 to 2024-01-28] Periods: None Scenarios: None Status: ✓ Components (7 items) -------------------- * Demand_A * Demand_B * ElecBoiler_B * ElecGrid * GasBoiler_A * GasSupply * Pipe_A_to_B Buses (4 items) --------------- * Electricity * Gas * Heat_A * Heat_B Effects (2 items) ----------------- * costs * Penalty Flows (10 items) ---------------- * Demand_A(Heat) * Demand_B(Heat) * ElecBoiler_B(Elec) * ElecBoiler_B(Heat) * ElecGrid(Elec) * GasBoiler_A(Gas) * GasBoiler_A(Heat) * GasSupply(Gas) * Pipe_A_to_B(from_A) * Pipe_A_to_B(to_B)
In [5]:
Copied!
# View results
print(f'Total cost: {fs_unidirectional.solution["costs"].item():.2f} €')
# View results print(f'Total cost: {fs_unidirectional.solution["costs"].item():.2f} €')
Total cost: 2479.00 €
In [6]:
Copied!
# Heat balance at Site A
fs_unidirectional.statistics.plot.balance('Heat_A')
# Heat balance at Site A fs_unidirectional.statistics.plot.balance('Heat_A')
Out[6]:
In [7]:
Copied!
# Heat balance at Site B
fs_unidirectional.statistics.plot.balance('Heat_B')
# Heat balance at Site B fs_unidirectional.statistics.plot.balance('Heat_B')
Out[7]:
In [8]:
Copied!
# Energy flow overview
fs_unidirectional.statistics.plot.sankey.flows()
# Energy flow overview fs_unidirectional.statistics.plot.sankey.flows()
Out[8]:
Observations¶
- The optimizer uses the cheaper gas boiler at Site A as much as possible
- Heat is transmitted to Site B (despite 5% losses) because gas is much cheaper than electricity
- The electric boiler at Site B only runs when transmission capacity is insufficient
Example 2: Bidirectional Transmission¶
Now allow heat to flow in both directions. This is useful when:
- Both sites have generation capacity
- Demand patterns differ between sites
- Prices or availability vary over time
In [9]:
Copied!
# Add a heat pump at Site B (cheaper during certain hours)
# Electricity price varies: cheap at night, expensive during day
elec_price = np.where(
(hour_of_day >= 22) | (hour_of_day <= 6),
0.08, # Night: 0.08 €/kWh
0.25, # Day: 0.25 €/kWh
)
# Add a heat pump at Site B (cheaper during certain hours) # Electricity price varies: cheap at night, expensive during day elec_price = np.where( (hour_of_day >= 22) | (hour_of_day <= 6), 0.08, # Night: 0.08 €/kWh 0.25, # Day: 0.25 €/kWh )
In [10]:
Copied!
fs_bidirectional = fx.FlowSystem(timesteps)
fs_bidirectional.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
fs_bidirectional.add_elements(
# === Buses ===
fx.Bus('Heat_A', carrier='heat'),
fx.Bus('Heat_B', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
fx.Bus('Electricity', carrier='electricity'),
# === Effect ===
fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True),
# === External supplies ===
fx.Source('GasSupply', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour=0.06)]),
fx.Source('ElecGrid', outputs=[fx.Flow('Elec', bus='Electricity', size=500, effects_per_flow_hour=elec_price)]),
# === Site A: Gas boiler ===
fx.LinearConverter(
'GasBoiler_A',
inputs=[fx.Flow('Gas', bus='Gas', size=500)],
outputs=[fx.Flow('Heat', bus='Heat_A', size=400)],
conversion_factors=[{'Gas': 1, 'Heat': 0.92}],
),
# === Site B: Heat pump (efficient with variable electricity price) ===
fx.LinearConverter(
'HeatPump_B',
inputs=[fx.Flow('Elec', bus='Electricity', size=100)],
outputs=[fx.Flow('Heat', bus='Heat_B', size=350)],
conversion_factors=[{'Elec': 1, 'Heat': 3.5}], # COP = 3.5
),
# === BIDIRECTIONAL Transmission ===
fx.Transmission(
'Pipe_AB',
# Direction 1: A → B
in1=fx.Flow('from_A', bus='Heat_A', size=200),
out1=fx.Flow('to_B', bus='Heat_B', size=200),
# Direction 2: B → A
in2=fx.Flow('from_B', bus='Heat_B', size=200),
out2=fx.Flow('to_A', bus='Heat_A', size=200),
relative_losses=0.05,
prevent_simultaneous_flows_in_both_directions=True, # Can't flow both ways at once
),
# === Demands ===
fx.Sink('Demand_A', inputs=[fx.Flow('Heat', bus='Heat_A', size=1, fixed_relative_profile=demand_a)]),
fx.Sink('Demand_B', inputs=[fx.Flow('Heat', bus='Heat_B', size=1, fixed_relative_profile=demand_b)]),
)
fs_bidirectional.optimize(fx.solvers.HighsSolver())
fs_bidirectional = fx.FlowSystem(timesteps) fs_bidirectional.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) fs_bidirectional.add_elements( # === Buses === fx.Bus('Heat_A', carrier='heat'), fx.Bus('Heat_B', carrier='heat'), fx.Bus('Gas', carrier='gas'), fx.Bus('Electricity', carrier='electricity'), # === Effect === fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True), # === External supplies === fx.Source('GasSupply', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour=0.06)]), fx.Source('ElecGrid', outputs=[fx.Flow('Elec', bus='Electricity', size=500, effects_per_flow_hour=elec_price)]), # === Site A: Gas boiler === fx.LinearConverter( 'GasBoiler_A', inputs=[fx.Flow('Gas', bus='Gas', size=500)], outputs=[fx.Flow('Heat', bus='Heat_A', size=400)], conversion_factors=[{'Gas': 1, 'Heat': 0.92}], ), # === Site B: Heat pump (efficient with variable electricity price) === fx.LinearConverter( 'HeatPump_B', inputs=[fx.Flow('Elec', bus='Electricity', size=100)], outputs=[fx.Flow('Heat', bus='Heat_B', size=350)], conversion_factors=[{'Elec': 1, 'Heat': 3.5}], # COP = 3.5 ), # === BIDIRECTIONAL Transmission === fx.Transmission( 'Pipe_AB', # Direction 1: A → B in1=fx.Flow('from_A', bus='Heat_A', size=200), out1=fx.Flow('to_B', bus='Heat_B', size=200), # Direction 2: B → A in2=fx.Flow('from_B', bus='Heat_B', size=200), out2=fx.Flow('to_A', bus='Heat_A', size=200), relative_losses=0.05, prevent_simultaneous_flows_in_both_directions=True, # Can't flow both ways at once ), # === Demands === fx.Sink('Demand_A', inputs=[fx.Flow('Heat', bus='Heat_A', size=1, fixed_relative_profile=demand_a)]), fx.Sink('Demand_B', inputs=[fx.Flow('Heat', bus='Heat_B', size=1, fixed_relative_profile=demand_b)]), ) fs_bidirectional.optimize(fx.solvers.HighsSolver())
Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms
MIP linopy-problem-p5a6fas_ has 2876 rows; 3044 cols; 9096 nonzeros; 336 integer variables (336 binary)
Coefficient ranges:
Matrix [1e-05, 2e+02]
Cost [1e+00, 1e+00]
Bound [1e+00, 1e+03]
RHS [1e+00, 1e+00]
Presolving model
1176 rows, 1008 cols, 2688 nonzeros 0s
1008 rows, 840 cols, 2520 nonzeros 0s
529 rows, 488 cols, 1248 nonzeros 0s
67 rows, 134 cols, 134 nonzeros 0s
0 rows, 0 cols, 0 nonzeros 0s
Presolve reductions: rows 0(-2876); columns 0(-3044); nonzeros 0(-9096) - Reduced to empty
Presolve: Optimal
Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero
Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work
Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time
0 0 0 0.00% 2479.002947 2479.002947 0.00% 0 0 0 0 0.0s
Solving report
Model linopy-problem-p5a6fas_
Status Optimal
Primal bound 2479.00294737
Dual bound 2479.00294737
Gap 0% (tolerance: 1%)
P-D integral 0
Solution status feasible
2479.00294737 (objective)
0 (bound viol.)
0 (int. viol.)
0 (row viol.)
Timing 0.01
Max sub-MIP depth 0
Nodes 0
Repair LPs 0
LP iterations 0
Out[10]:
FlowSystem ========== Timesteps: 168 (Hour) [2024-01-22 to 2024-01-28] Periods: None Scenarios: None Status: ✓ Components (7 items) -------------------- * Demand_A * Demand_B * ElecGrid * GasBoiler_A * GasSupply * HeatPump_B * Pipe_AB Buses (4 items) --------------- * Electricity * Gas * Heat_A * Heat_B Effects (2 items) ----------------- * costs * Penalty Flows (12 items) ---------------- * Demand_A(Heat) * Demand_B(Heat) * ElecGrid(Elec) * GasBoiler_A(Gas) * GasBoiler_A(Heat) * GasSupply(Gas) * HeatPump_B(Elec) * HeatPump_B(Heat) * Pipe_AB(from_A) * Pipe_AB(from_B) ... (+2 more)
In [11]:
Copied!
# Compare costs
print(f'Unidirectional cost: {fs_unidirectional.solution["costs"].item():.2f} €')
print(f'Bidirectional cost: {fs_bidirectional.solution["costs"].item():.2f} €')
savings = fs_unidirectional.solution['costs'].item() - fs_bidirectional.solution['costs'].item()
print(f'Savings from bidirectional: {savings:.2f} €')
# Compare costs print(f'Unidirectional cost: {fs_unidirectional.solution["costs"].item():.2f} €') print(f'Bidirectional cost: {fs_bidirectional.solution["costs"].item():.2f} €') savings = fs_unidirectional.solution['costs'].item() - fs_bidirectional.solution['costs'].item() print(f'Savings from bidirectional: {savings:.2f} €')
Unidirectional cost: 2479.00 € Bidirectional cost: 2479.00 € Savings from bidirectional: 0.00 €
In [12]:
Copied!
# Visualize transmission flows in both directions using xarray
flow_data = xr.Dataset(
{
'A_to_B': fs_bidirectional.solution['Pipe_AB(from_A)|flow_rate'],
'B_to_A': fs_bidirectional.solution['Pipe_AB(from_B)|flow_rate'],
}
)
fig = px.line(
x=list(flow_data['time'].values) * 2,
y=np.concatenate([flow_data['A_to_B'].values, flow_data['B_to_A'].values]),
color=['A → B'] * len(flow_data['time']) + ['B → A'] * len(flow_data['time']),
title='Transmission Flow Direction Over Time',
labels={'x': 'Time', 'y': 'Flow Rate [kW]', 'color': 'Direction'},
)
fig
# Visualize transmission flows in both directions using xarray flow_data = xr.Dataset( { 'A_to_B': fs_bidirectional.solution['Pipe_AB(from_A)|flow_rate'], 'B_to_A': fs_bidirectional.solution['Pipe_AB(from_B)|flow_rate'], } ) fig = px.line( x=list(flow_data['time'].values) * 2, y=np.concatenate([flow_data['A_to_B'].values, flow_data['B_to_A'].values]), color=['A → B'] * len(flow_data['time']) + ['B → A'] * len(flow_data['time']), title='Transmission Flow Direction Over Time', labels={'x': 'Time', 'y': 'Flow Rate [kW]', 'color': 'Direction'}, ) fig
In [13]:
Copied!
# Heat balance at Site B showing bidirectional flows
fs_bidirectional.statistics.plot.balance('Heat_B')
# Heat balance at Site B showing bidirectional flows fs_bidirectional.statistics.plot.balance('Heat_B')
Out[13]:
In [14]:
Copied!
# Energy flow overview
fs_bidirectional.statistics.plot.sankey.flows()
# Energy flow overview fs_bidirectional.statistics.plot.sankey.flows()
Out[14]:
Observations¶
- During cheap electricity hours (night): Heat pump at Site B produces heat, some flows to Site A
- During expensive electricity hours (day): Gas boiler at Site A supplies both sites
- The bidirectional transmission enables load shifting and arbitrage between sites
Example 3: Transmission Capacity Optimization¶
What's the optimal pipe capacity? Let the optimizer decide.
In [15]:
Copied!
# Daily amortized pipe cost (simplified)
PIPE_COST_PER_KW = 0.05 # €/kW/day capacity cost
fs_invest = fx.FlowSystem(timesteps)
fs_invest.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
fs_invest.add_elements(
# === Buses ===
fx.Bus('Heat_A', carrier='heat'),
fx.Bus('Heat_B', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
fx.Bus('Electricity', carrier='electricity'),
# === Effect ===
fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True),
# === External supplies ===
fx.Source('GasSupply', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour=0.06)]),
fx.Source('ElecGrid', outputs=[fx.Flow('Elec', bus='Electricity', size=500, effects_per_flow_hour=elec_price)]),
# === Site A: Gas boiler ===
fx.LinearConverter(
'GasBoiler_A',
inputs=[fx.Flow('Gas', bus='Gas', size=500)],
outputs=[fx.Flow('Heat', bus='Heat_A', size=400)],
conversion_factors=[{'Gas': 1, 'Heat': 0.92}],
),
# === Site B: Heat pump ===
fx.LinearConverter(
'HeatPump_B',
inputs=[fx.Flow('Elec', bus='Electricity', size=100)],
outputs=[fx.Flow('Heat', bus='Heat_B', size=350)],
conversion_factors=[{'Elec': 1, 'Heat': 3.5}],
),
# === Site B: Backup electric boiler ===
fx.LinearConverter(
'ElecBoiler_B',
inputs=[fx.Flow('Elec', bus='Electricity', size=200)],
outputs=[fx.Flow('Heat', bus='Heat_B', size=200)],
conversion_factors=[{'Elec': 1, 'Heat': 0.99}],
),
# === Transmission with INVESTMENT OPTIMIZATION ===
# Investment parameters are passed via 'size' parameter
fx.Transmission(
'Pipe_AB',
in1=fx.Flow(
'from_A',
bus='Heat_A',
size=fx.InvestParameters(
effects_of_investment_per_size={'costs': PIPE_COST_PER_KW * 7}, # Weekly cost
minimum_size=0,
maximum_size=300,
),
),
out1=fx.Flow('to_B', bus='Heat_B'),
in2=fx.Flow(
'from_B',
bus='Heat_B',
size=fx.InvestParameters(
effects_of_investment_per_size={'costs': PIPE_COST_PER_KW * 7},
minimum_size=0,
maximum_size=300,
),
),
out2=fx.Flow('to_A', bus='Heat_A'),
relative_losses=0.05,
balanced=True, # Same capacity in both directions
prevent_simultaneous_flows_in_both_directions=True,
),
# === Demands ===
fx.Sink('Demand_A', inputs=[fx.Flow('Heat', bus='Heat_A', size=1, fixed_relative_profile=demand_a)]),
fx.Sink('Demand_B', inputs=[fx.Flow('Heat', bus='Heat_B', size=1, fixed_relative_profile=demand_b)]),
)
fs_invest.optimize(fx.solvers.HighsSolver())
# Daily amortized pipe cost (simplified) PIPE_COST_PER_KW = 0.05 # €/kW/day capacity cost fs_invest = fx.FlowSystem(timesteps) fs_invest.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) fs_invest.add_elements( # === Buses === fx.Bus('Heat_A', carrier='heat'), fx.Bus('Heat_B', carrier='heat'), fx.Bus('Gas', carrier='gas'), fx.Bus('Electricity', carrier='electricity'), # === Effect === fx.Effect('costs', '€', 'Operating Costs', is_standard=True, is_objective=True), # === External supplies === fx.Source('GasSupply', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour=0.06)]), fx.Source('ElecGrid', outputs=[fx.Flow('Elec', bus='Electricity', size=500, effects_per_flow_hour=elec_price)]), # === Site A: Gas boiler === fx.LinearConverter( 'GasBoiler_A', inputs=[fx.Flow('Gas', bus='Gas', size=500)], outputs=[fx.Flow('Heat', bus='Heat_A', size=400)], conversion_factors=[{'Gas': 1, 'Heat': 0.92}], ), # === Site B: Heat pump === fx.LinearConverter( 'HeatPump_B', inputs=[fx.Flow('Elec', bus='Electricity', size=100)], outputs=[fx.Flow('Heat', bus='Heat_B', size=350)], conversion_factors=[{'Elec': 1, 'Heat': 3.5}], ), # === Site B: Backup electric boiler === fx.LinearConverter( 'ElecBoiler_B', inputs=[fx.Flow('Elec', bus='Electricity', size=200)], outputs=[fx.Flow('Heat', bus='Heat_B', size=200)], conversion_factors=[{'Elec': 1, 'Heat': 0.99}], ), # === Transmission with INVESTMENT OPTIMIZATION === # Investment parameters are passed via 'size' parameter fx.Transmission( 'Pipe_AB', in1=fx.Flow( 'from_A', bus='Heat_A', size=fx.InvestParameters( effects_of_investment_per_size={'costs': PIPE_COST_PER_KW * 7}, # Weekly cost minimum_size=0, maximum_size=300, ), ), out1=fx.Flow('to_B', bus='Heat_B'), in2=fx.Flow( 'from_B', bus='Heat_B', size=fx.InvestParameters( effects_of_investment_per_size={'costs': PIPE_COST_PER_KW * 7}, minimum_size=0, maximum_size=300, ), ), out2=fx.Flow('to_A', bus='Heat_A'), relative_losses=0.05, balanced=True, # Same capacity in both directions prevent_simultaneous_flows_in_both_directions=True, ), # === Demands === fx.Sink('Demand_A', inputs=[fx.Flow('Heat', bus='Heat_A', size=1, fixed_relative_profile=demand_a)]), fx.Sink('Demand_B', inputs=[fx.Flow('Heat', bus='Heat_B', size=1, fixed_relative_profile=demand_b)]), ) fs_invest.optimize(fx.solvers.HighsSolver())
Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms
MIP linopy-problem-mf8yihjb has 3725 rows; 3388 cols; 11130 nonzeros; 338 integer variables (338 binary)
Coefficient ranges:
Matrix [1e-05, 3e+02]
Cost [1e+00, 1e+00]
Bound [1e+00, 1e+03]
RHS [1e+00, 1e+00]
Presolving model
1516 rows, 1179 cols, 3536 nonzeros 0s
1180 rows, 843 cols, 4376 nonzeros 0s
1180 rows, 837 cols, 4340 nonzeros 0s
Presolve reductions: rows 1180(-2545); columns 837(-2551); nonzeros 4340(-6790)
Solving MIP model with:
1180 rows
837 cols (338 binary, 0 integer, 0 implied int., 499 continuous, 0 domain fixed)
4340 nonzeros
Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero
Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work
Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time
J 0 0 0 0.00% -inf 4073.636083 Large 0 0 0 0 0.0s
0 0 0 0.00% 2611.634526 4073.636083 35.89% 0 0 0 455 0.0s
C 0 0 0 0.00% 2611.634526 3531.562484 26.05% 516 170 0 629 0.1s
L 0 0 0 0.00% 2611.634526 2611.634526 0.00% 516 170 0 629 0.1s
1 0 1 100.00% 2611.634526 2611.634526 0.00% 516 170 0 698 0.1s
Solving report
Model linopy-problem-mf8yihjb
Status Optimal
Primal bound 2611.63452632
Dual bound 2611.63452632
Gap 0% (tolerance: 1%)
P-D integral 0.0248243788296
Solution status feasible
2611.63452632 (objective)
0 (bound viol.)
0 (int. viol.)
0 (row viol.)
Timing 0.11
Max sub-MIP depth 1
Nodes 1
Repair LPs 0
LP iterations 698
0 (strong br.)
174 (separation)
69 (heuristics)
Out[15]:
FlowSystem ========== Timesteps: 168 (Hour) [2024-01-22 to 2024-01-28] Periods: None Scenarios: None Status: ✓ Components (8 items) -------------------- * Demand_A * Demand_B * ElecBoiler_B * ElecGrid * GasBoiler_A * GasSupply * HeatPump_B * Pipe_AB Buses (4 items) --------------- * Electricity * Gas * Heat_A * Heat_B Effects (2 items) ----------------- * costs * Penalty Flows (14 items) ---------------- * Demand_A(Heat) * Demand_B(Heat) * ElecBoiler_B(Elec) * ElecBoiler_B(Heat) * ElecGrid(Elec) * GasBoiler_A(Gas) * GasBoiler_A(Heat) * GasSupply(Gas) * HeatPump_B(Elec) * HeatPump_B(Heat) ... (+4 more)
In [16]:
Copied!
# Results
optimal_capacity = fs_invest.solution['Pipe_AB(from_A)|size'].item()
total_cost = fs_invest.solution['costs'].item()
print(f'Optimal pipe capacity: {optimal_capacity:.1f} kW')
print(f'Total cost: {total_cost:.2f} €')
# Results optimal_capacity = fs_invest.solution['Pipe_AB(from_A)|size'].item() total_cost = fs_invest.solution['costs'].item() print(f'Optimal pipe capacity: {optimal_capacity:.1f} kW') print(f'Total cost: {total_cost:.2f} €')
Optimal pipe capacity: 189.5 kW Total cost: 2611.63 €
In [17]:
Copied!
# Effect breakdown by component
fs_invest.statistics.plot.effects()
# Effect breakdown by component fs_invest.statistics.plot.effects()
Out[17]:
In [18]:
Copied!
# Energy flows
fs_invest.statistics.plot.sankey.flows()
# Energy flows fs_invest.statistics.plot.sankey.flows()
Out[18]:
Key Concepts¶
Transmission Component Structure¶
fx.Transmission(
label='pipe_name',
# Direction 1: A → B
in1=fx.Flow('from_A', bus='Bus_A', size=100),
out1=fx.Flow('to_B', bus='Bus_B', size=100),
# Direction 2: B → A (optional - omit for unidirectional)
in2=fx.Flow('from_B', bus='Bus_B', size=100),
out2=fx.Flow('to_A', bus='Bus_A', size=100),
# Loss parameters
relative_losses=0.05, # 5% proportional loss
absolute_losses=10, # 10 kW fixed loss when active (optional)
# Operational constraints
prevent_simultaneous_flows_in_both_directions=True,
balanced=True, # Same capacity both directions (needs InvestParameters)
)
Loss Types¶
| Loss Type | Formula | Use Case |
|---|---|---|
| Relative | out = in × (1 - loss) | Heat pipes, electrical lines |
| Absolute | out = in - loss (when active) | Pump energy, standby losses |
Bidirectional vs Unidirectional¶
| Configuration | Parameters | Use Case |
|---|---|---|
| Unidirectional | in1, out1 only | One-way pipelines, conveyors |
| Bidirectional | in1, out1, in2, out2 | Power lines, reversible pipes |
Investment Optimization¶
Use InvestParameters as the size parameter for capacity optimization:
in1=fx.Flow(
'from_A',
bus='Bus_A',
size=fx.InvestParameters( # Pass InvestParameters as size
effects_of_investment_per_size={'costs': cost_per_kw},
minimum_size=0,
maximum_size=500,
),
)
Common Use Cases¶
| Application | Typical Losses | Notes |
|---|---|---|
| District heating pipe | 2-10% relative | Temperature-dependent |
| High voltage line | 1-5% relative | Distance-dependent |
| Natural gas pipeline | 0.5-2% relative | Compressor energy as absolute loss |
| Conveyor belt | Fixed absolute | Motor energy consumption |
| Hydrogen pipeline | 1-3% relative | Compression losses |
Summary¶
You learned how to:
- Create unidirectional transmission between two buses
- Model bidirectional transmission with flow direction constraints
- Apply relative and absolute losses to transmission
- Optimize transmission capacity using InvestParameters
- Analyze multi-site energy systems with interconnections
Next Steps¶
- 07-scenarios-and-periods: Multi-year planning with uncertainty
- 08a-Aggregation: Speed up large problems with time series aggregation
- 08b-Rolling Horizon: Decompose large problems into sequential segments